//
// Copyright 2017 Google Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

@use 'sass:list';
@use 'sass:map';
@use 'sass:meta';
@use '@material/feature-targeting/feature-targeting';
@use './css';
@use './custom-properties';
@use './gss';
@use './keys';
@use './replace';
@use './theme-color';

@mixin core-styles($query: feature-targeting.all()) {
  $feat-color: feature-targeting.create-target($query, color);

  :root {
    @include feature-targeting.targets($feat-color) {
      @each $style in theme-color.get-theme-keys() {
        @include custom-properties.declaration(
          keys.create-custom-property($style)
        );
      }
    }
  }

  @each $style in theme-color.get-theme-keys() {
    @if $style != 'background' and $style != 'surface' {
      .mdc-theme--#{$style} {
        @include feature-targeting.targets($feat-color) {
          @include property(color, $style);
        }
      }
    } @else {
      .mdc-theme--#{$style} {
        @include feature-targeting.targets($feat-color) {
          @include property(background-color, $style);
        }
      }
    }
  }

  // CSS rules for using primary and secondary (plus light/dark variants) as background colors.
  @each $style in ('primary', 'secondary') {
    .mdc-theme--#{$style}-bg {
      @include feature-targeting.targets($feat-color) {
        @include property(background-color, $style);
      }
    }
  }
}

/// Applies a dynamic value to the specified property. This mixin should be used
/// in theme style mixins when setting properties.
///
/// The value may be any of the following:
/// - a standard CSS value
/// - a custom property Map, e.g. (varname: --mdc-foo, fallback: blue)
/// - a Material theme key String, e.g. 'primary', 'on-primary'
///
/// @example
///   @include theme.property(color, teal);
///   @include theme.property(color, custom-properties.create(foo, blue));
///   @include theme.property(color, primary);
///
/// A `$replace` Map parameter may be provided to replace key/value pairs for
/// string values. This can be used to substitute parameters in complex string
/// values such as `calc()` with custom properties.
///
/// @example
///   @include theme.property(
///     width,
///     calc(foo + bar),
///     $replace: (foo: custom-properties.create(foo), bar: 8px)
///   );
///
/// Note: Material theme key Strings (e.g. `primary`) are not supported as
/// replacement values.
///
/// A CSS custom property declaration may be emitted by providing a custom
/// property Map to `$property`. The fallback value (or `$value` if provided)
/// will be used as the declaration value.
///
/// @example - scss
///   .foo {
///     @include theme.property(custom-properties.create(foo, teal));
///     @include theme.property(custom-properties.create(bar, teal), blue);
///   }
///
/// @example - css
///   .foo {
///     --mdc-foo: teal;
///     --mdc-bar: blue;
///   }
///
/// @param {String | Map} $property - The name of the CSS property. May also be
///     a custom property Map to emit a custom propery declaration.
/// @param {String | Number | Color | List | Map} $value - The property's value.
///     This parameter may be omitted if `$property` is a custom property Map.
/// @param {Map} $gss - Optional Map of GSS annotations to set.
/// @param {Map} $replace - An optional Map of replacement key/value pairs if
///     the `$value` is a string.
/// @param {Bool} $important - Set to true to add an `!important` rule. Defaults
///     to false.
@mixin property(
  $property,
  $value: null,
  $gss: (),
  $replace: null,
  $important: false
) {
  @if custom-properties.is-custom-prop($property) {
    // $property is a custom property Map
    //   --mdc-foo: value;
    @if $value {
      $property: custom-properties.set-fallback(
        $property,
        $value,
        $shallow: true
      );
    }

    @include custom-properties.declaration(
      $property,
      $gss: $gss,
      $important: $important
    );
  } @else if custom-properties.is-custom-prop($value) {
    // $value is a custom property Map
    //   property: var(--mdc-foo, fallback);
    @include custom-properties.declaration(
      $property,
      $value,
      $gss: $gss,
      $important: $important
    );
  } @else if keys.is-key($value) {
    // $value is a key String
    //   property: key;
    $custom-prop: keys.create-custom-property($value);

    @if theme-color.is-theme-key($value) {
      // Determine if we need to use a compile-time updated value to support
      // Angular.
      $key: $value;
      // (changed: Bool, value: *)
      $result: theme-color.deprecated-get-global-theme-key-value-if-changed(
        $key
      );

      @if map.get($result, changed) {
        // $mdc-theme-property-values was changed at compile time. Use the
        // global value instead. Otherwise if it was not changed, continue
        // using the key store normally.
        $custom-prop: keys.create-custom-property($key);
        $custom-prop: custom-properties.set-fallback(
          $custom-prop,
          map.get($result, value)
        );
      }
    }

    @include custom-properties.declaration(
      $property,
      $custom-prop,
      $gss: $gss,
      $important: $important
    );
  } @else {
    // $value is a standard CSS value
    //   property: value;
    $fallback: null;
    @if $replace {
      // If any replacements are null, treat the entire value as null (do not
      // emit anything).
      @each $name, $replacement in $replace {
        @if $replacement == null {
          $value: null;
        }
      }
    }

    @if $replace and $value {
      @if meta.type-of($replace) != 'map' {
        @error 'mdc-theme: Invalid replacement #{$replace}. Must be a Map.';
      }

      $replace-map-fallback: ();
      $replace-map-value: ();
      $needs-fallback: false;
      @each $name, $replacement in $replace {
        @if custom-properties.is-custom-prop($replacement) {
          $replace-value: custom-properties.get-declaration-value($replacement);
          $replace-fallback: custom-properties.get-declaration-fallback(
            $replacement
          );
          @if $replace-fallback {
            $needs-fallback: true;
          }

          $replace-map-value: map.set(
            $replace-map-value,
            $name,
            $replace-value
          );
          $replace-map-fallback: map.set(
            $replace-map-fallback,
            $name,
            $replace-fallback
          );
        } @else {
          $replace-map-value: map.set($replace-map-value, $name, $replacement);
          $replace-map-fallback: map.set(
            $replace-map-fallback,
            $name,
            $replacement
          );
        }
      }

      @if meta.type-of($value) == 'string' {
        @if $needs-fallback {
          $fallback: replace.replace-string($value, $replace-map-fallback);
        }
        $value: replace.replace-string($value, $replace-map-value);
      } @else if meta.type-of($value) == 'list' {
        @if $needs-fallback {
          $fallback: replace.replace-list($value, $replace-map-fallback);
        }
        $value: replace.replace-list($value, $replace-map-value);
      } @else {
        @error 'mdc-theme: Invalid replacement value #{$value}. $replace may only be used with string or list values.';
      }
    }

    @include css.declaration(
      $property,
      $value,
      $fallback-value: $fallback,
      $gss: $gss,
      $important: $important
    );
  }
}

// @deprecated use the `property()` mixin instead
@mixin prop($property, $style) {
  @include property($property, $style);
}

/// Validates theme configuration keys by comparing it with original theme
/// configuration, also validates theme values to see if it has any unsupported
/// value formats.
///
/// Use this in internal `theme()` mixins to validate library-provided
/// `$theme` maps and ensure that all tokens are correct and present.
///
/// @example
///     @mixin theme($theme) {
///       @include theme.validate-theme($light-theme, $theme);
///       ...
///     }
///
/// @see validate-theme-styles to validate only theme keys.
///
/// @param {Map} $origin-theme - Original theme configuration in Sass map format
///     that has all supported keys.
/// @param {Map} $custom-theme - Provided theme configuration in Sass map format
///     that should be validated against `$origin-theme`.
@mixin validate-theme($origin-theme, $custom-theme, $test-only: false) {
  @include validate-theme-styles(
    $origin-theme,
    $custom-theme,
    $test-only: $test-only
  );
  @include _validate-theme-values($custom-theme, $test-only: $test-only);
}

/// Validates theme configuration keys by comparing it with original theme
/// configuration.
///
/// Use this in internal `theme-styles()` mixins to validate library-provided
/// `$theme` maps and ensure that all tokens are correct and present.
///
/// @example
///     @mixin theme-styles($theme) {
///       @include theme.validate-theme-styles($light-theme, $theme);
///       ...
///     }
///
/// @see validate-theme to validate both theme keys and theme values.
///
/// @param {Map} $origin-theme - Original theme configuration in Sass map format
///     that has all supported keys.
/// @param {Map} $custom-theme - Provided theme configuration in Sass map format
///     that should be validated against `$origin-theme`.
@mixin validate-theme-styles($origin-theme, $custom-theme, $test-only: false) {
  $origin-keys: map.keys($origin-theme);
  $unsupported-keys: ();

  @each $key, $value in $custom-theme {
    @if (not list.index($origin-keys, $key)) {
      $unsupported-keys: list.append(
        $unsupported-keys,
        $key,
        $separator: comma
      );
    }
  }

  @if list.length($unsupported-keys) > 0 {
    $error-message: 'Unsupported keys found: #{$unsupported-keys}. Expected one of: #{$origin-keys}.';

    @if $test-only {
      content: $error-message;
    } @else {
      @error $error-message;
    }
  }
}

/// Validates theme configuration values to see if it has any unsupported value
/// formats.
/// @see Use `validate-theme()` to validate both theme keys and theme values.
/// @param {Map} $custom-theme - Provided theme configuration in Sass map format
///     that needs to be validated.
@mixin _validate-theme-values($custom-theme, $test-only: false) {
  $unsupported-custom-prop-keys: ();

  @each $key, $value in $custom-theme {
    @if custom-properties.is-custom-prop($value) {
      $unsupported-custom-prop-keys: list.append(
        $unsupported-custom-prop-keys,
        $key,
        $separator: comma
      );
    }
  }

  @if list.length($unsupported-custom-prop-keys) > 0 {
    $error-message: 'Custom properties are not supported for theme map keys: #{$unsupported-custom-prop-keys}';

    @if $test-only {
      content: $error-message;
    } @else {
      @error $error-message;
    }
  }
}
